1
|
|
|
/*! |
2
|
|
|
* @name ElkArte Forum |
3
|
|
|
* @copyright ElkArte Forum contributors |
4
|
|
|
* @license BSD http://opensource.org/licenses/BSD-3-Clause |
5
|
|
|
* |
6
|
|
|
* @version 1.1 |
7
|
|
|
*/ |
8
|
|
|
|
9
|
|
|
/** |
10
|
|
|
* This file contains javascript associated with the atwho function as it |
11
|
|
|
* relates to an sceditor invocation |
12
|
|
|
*/ |
13
|
|
|
var disableDrafts = false; |
14
|
|
|
|
15
|
|
|
(function($, window, document) { |
|
|
|
|
16
|
|
|
'use strict'; |
17
|
|
|
|
18
|
|
|
// Editor instance |
19
|
|
|
var editor, |
20
|
|
|
rangeHelper; |
21
|
|
|
|
22
|
|
|
function elk_Mentions(options) { |
23
|
|
|
// All the passed options and defaults are loaded to the opts object |
24
|
|
|
this.opts = $.extend({}, this.defaults, options); |
|
|
|
|
25
|
|
|
} |
26
|
|
|
|
27
|
|
|
elk_Mentions.prototype.attachAtWho = function(oMentions, $element, oIframeWindow) { |
28
|
|
|
var mentioned = $('#mentioned'); |
29
|
|
|
|
30
|
|
|
// Create / use a container to hold the results |
31
|
|
|
if (mentioned.length === 0) |
32
|
|
|
$('#' + oMentions.opts.editor_id).after(oMentions.opts._mentioned); |
33
|
|
|
else |
34
|
|
|
oMentions.opts._mentioned = mentioned; |
35
|
|
|
|
36
|
|
|
oMentions.opts.cache.mentions = this.opts._mentioned; |
37
|
|
|
|
38
|
|
|
$element.atwho({ |
39
|
|
|
at: "@", |
40
|
|
|
limit: 8, |
41
|
|
|
maxLen: 25, |
42
|
|
|
displayTpl: "<li data-value='${atwho-at}${name}' data-id='${id}'>${name}</li>", |
43
|
|
|
acceptSpaceBar: true, |
44
|
|
|
callbacks: { |
45
|
|
|
filter: function (query, items, search_key) { |
|
|
|
|
46
|
|
|
// Already cached this query, then use it |
47
|
|
|
if (typeof oMentions.opts.cache.names[query] !== 'undefined') { |
48
|
|
|
return oMentions.opts.cache.names[query]; |
49
|
|
|
} |
50
|
|
|
|
51
|
|
|
return []; |
52
|
|
|
}, |
53
|
|
|
// Well then lets make a find member suggest call |
54
|
|
|
remoteFilter: function(query, callback) { |
55
|
|
|
// Let be easy-ish on the server, don't go looking until we have at least two characters |
56
|
|
|
if (query.length < 2) |
57
|
|
|
return; |
58
|
|
|
|
59
|
|
|
// No slamming the server either |
60
|
|
|
var current_call = parseInt(new Date().getTime() / 1000); |
61
|
|
|
if (oMentions.opts._last_call !== 0 && oMentions.opts._last_call + 0.5 > current_call) { |
62
|
|
|
callback(oMentions.opts._names); |
63
|
|
|
return; |
64
|
|
|
} |
65
|
|
|
|
66
|
|
|
// What we want |
67
|
|
|
var obj = { |
68
|
|
|
"suggest_type": "member", |
69
|
|
|
"search": query.php_urlencode(), |
70
|
|
|
"time": current_call |
71
|
|
|
}; |
72
|
|
|
|
73
|
|
|
// Make the request |
74
|
|
|
suggest(obj, function() { |
75
|
|
|
// Update the time gate |
76
|
|
|
oMentions.opts._last_call = current_call; |
77
|
|
|
|
78
|
|
|
// Update the cache with the values for reuse in local filter |
79
|
|
|
oMentions.opts.cache.names[query] = oMentions.opts._names; |
80
|
|
|
|
81
|
|
|
// Update the query cache for use in revalidateMentions |
82
|
|
|
oMentions.opts.cache.queries[oMentions.opts.cache.queries.length] = query; |
83
|
|
|
|
84
|
|
|
callback(oMentions.opts._names); |
85
|
|
|
}); |
86
|
|
|
}, |
87
|
|
|
beforeInsert: function(value, $li) { |
88
|
|
|
oMentions.addUID($li.data('id'), $li.data('value')); |
89
|
|
|
|
90
|
|
|
return value; |
91
|
|
|
}, |
92
|
|
|
matcher: function(flag, subtext, should_startWithSpace, acceptSpaceBar) { |
93
|
|
|
var _a, _y, match, regexp, space; |
94
|
|
|
|
95
|
|
|
flag = flag.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); |
96
|
|
|
|
97
|
|
|
if (should_startWithSpace) { |
98
|
|
|
flag = '(?:^|\\s)' + flag; |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
// Allow À - ÿ |
102
|
|
|
_a = decodeURI("%C3%80"); |
103
|
|
|
_y = decodeURI("%C3%BF"); |
104
|
|
|
|
105
|
|
|
// Allow first last name entry? |
106
|
|
|
space = acceptSpaceBar ? "\ " : ""; |
107
|
|
|
|
108
|
|
|
// regexp = new RegExp(flag + '([^ <>&"\'=\\\\\n]*)$|' + flag + '([^\\x00-\\xff]*)$', 'gi'); |
109
|
|
|
regexp = new RegExp(flag + "([A-Za-z" + _a + "-" + _y + "0-9_" + space + "\\[\\]\'\.\+\-]*)$|" + flag + "([^\\x00-\\xff]*)$", 'gi'); |
110
|
|
|
match = regexp.exec(subtext); |
111
|
|
|
|
112
|
|
|
if (match) { |
113
|
|
|
return match[2] || match[1]; |
114
|
|
|
} |
115
|
|
|
else { |
|
|
|
|
116
|
|
|
return null; |
117
|
|
|
} |
118
|
|
|
}, |
119
|
|
|
highlighter: function(li, query) { |
120
|
|
|
var regexp; |
121
|
|
|
|
122
|
|
|
if (!query) |
123
|
|
|
return li; |
124
|
|
|
|
125
|
|
|
// Preg Quote regexp from http://phpjs.org/functions/preg_quote/ |
126
|
|
|
query = query.replace(new RegExp('[.\\\\+*?\\[\\^\\]$(){}=!<>|:\\-]', 'g'), '\\$&'); |
127
|
|
|
|
128
|
|
|
regexp = new RegExp(">\\s*(\\w*)(" + query.replace("+", "\\+") + ")(\\w*)\\s*<", 'ig'); |
129
|
|
|
return li.replace(regexp, function(str, $1, $2, $3) { |
130
|
|
|
return '> ' + $1 + '<strong>' + $2 + '</strong>' + $3 + ' <'; |
131
|
|
|
}); |
132
|
|
|
}, |
133
|
|
|
beforeReposition: function (offset) { |
134
|
|
|
// We only need to adjust when in wysiwyg |
135
|
|
|
if (editor.inSourceMode()) |
136
|
|
|
return offset; |
137
|
|
|
|
138
|
|
|
// Lets get the caret position so we can add the mentions box there |
139
|
|
|
var corrected_offset = findAtPosition(); |
140
|
|
|
|
141
|
|
|
offset.top = corrected_offset.top; |
142
|
|
|
offset.left = corrected_offset.left; |
143
|
|
|
|
144
|
|
|
return offset; |
145
|
|
|
} |
146
|
|
|
} |
147
|
|
|
}); |
148
|
|
|
|
149
|
|
|
// Use atwho selection box show/hide events to prevent autosave from firing |
150
|
|
|
$(oIframeWindow).on("shown.atwho", function(event, offset) { |
|
|
|
|
151
|
|
|
disableDrafts = true; |
152
|
|
|
}); |
153
|
|
|
|
154
|
|
|
$(oIframeWindow).on("hidden.atwho", function(event, offset) { |
|
|
|
|
155
|
|
|
disableDrafts = false; |
156
|
|
|
}); |
157
|
|
|
|
158
|
|
|
/** |
159
|
|
|
* Makes the ajax call for data, returns to callback function when done. |
160
|
|
|
* |
161
|
|
|
* @param obj values to pass to action suggest |
162
|
|
|
* @param callback function to call when we have completed our call |
163
|
|
|
*/ |
164
|
|
|
function suggest(obj, callback) |
165
|
|
|
{ |
166
|
|
|
var postString = "jsonString=" + JSON.stringify(obj) + "&" + elk_session_var + "=" + elk_session_id; |
|
|
|
|
167
|
|
|
|
168
|
|
|
oMentions.opts._names = []; |
169
|
|
|
|
170
|
|
|
$.ajax({ |
171
|
|
|
url: elk_scripturl + "?action=suggest;xml", |
|
|
|
|
172
|
|
|
type: "post", |
173
|
|
|
data: postString, |
174
|
|
|
dataType: "xml" |
175
|
|
|
}) |
176
|
|
|
.done(function(data) { |
177
|
|
|
$(data).find('item').each(function (idx, item) { |
178
|
|
|
if (typeof oMentions.opts._names[oMentions.opts._names.length] === 'undefined') |
179
|
|
|
oMentions.opts._names[oMentions.opts._names.length] = {}; |
180
|
|
|
|
181
|
|
|
oMentions.opts._names[oMentions.opts._names.length - 1] = { |
182
|
|
|
"id": $(item).attr('id'), |
183
|
|
|
"name": $(item).text() |
184
|
|
|
}; |
185
|
|
|
}); |
186
|
|
|
|
187
|
|
|
callback(); |
188
|
|
|
}) |
189
|
|
|
.fail(function(jqXHR, textStatus, errorThrown) { |
190
|
|
|
if ('console' in window) { |
191
|
|
|
window.console.info('Error:', textStatus, errorThrown.name); |
192
|
|
|
window.console.info(jqXHR.responseText); |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
callback(); |
196
|
|
|
}); |
197
|
|
|
} |
198
|
|
|
|
199
|
|
|
/** |
200
|
|
|
* Determine the caret position inside of sceditor's iframe |
201
|
|
|
* |
202
|
|
|
* What it does: |
203
|
|
|
* - Caret.js does not seem to return the correct position for (FF & IE) when |
204
|
|
|
* the iframe has vertically scrolled. |
205
|
|
|
* - This is an sceditor specific function to return a screen caret position |
206
|
|
|
* - Called just before At.js adds the mentions dropdown box |
207
|
|
|
* - Finds the @mentions tag and adds an invisible zero width space before it |
208
|
|
|
* - Gets the location offset() in the iframe "window" of the added space |
209
|
|
|
* - Adjusts for the iframe scroll |
210
|
|
|
* - Adds in the iframe container location offset() to main window |
211
|
|
|
* - Removes the space, restores the editor range. |
212
|
|
|
* |
213
|
|
|
* @returns {{}} |
214
|
|
|
*/ |
215
|
|
|
function findAtPosition() { |
216
|
|
|
// Get sceditor's RangeHelper for use |
217
|
|
|
rangeHelper = editor.getRangeHelper(); |
218
|
|
|
|
219
|
|
|
// Save the current state |
220
|
|
|
rangeHelper.saveRange(); |
221
|
|
|
|
222
|
|
|
var start = rangeHelper.getMarker('sceditor-start-marker'), |
223
|
|
|
parent = start.parentNode, |
224
|
|
|
prev = start.previousSibling, |
225
|
|
|
offset = {}, |
226
|
|
|
atPos, |
227
|
|
|
placefinder; |
228
|
|
|
|
229
|
|
|
// Create a placefinder span containing a 'ZERO WIDTH SPACE' Character |
230
|
|
|
placefinder = start.ownerDocument.createElement('span'); |
231
|
|
|
$(placefinder).text("200B").addClass('placefinder'); |
232
|
|
|
|
233
|
|
|
// Look back and find the mentions @ tag, so we can insert our span ahead of it |
234
|
|
|
while (prev) { |
235
|
|
|
atPos = (prev.nodeValue || '').lastIndexOf('@'); |
236
|
|
|
|
237
|
|
|
// Found the start of @mention |
238
|
|
|
if (atPos > -1) { |
239
|
|
|
parent.insertBefore(placefinder, prev.splitText(atPos + 1)); |
240
|
|
|
break; |
241
|
|
|
} |
242
|
|
|
|
243
|
|
|
prev = prev.previousSibling; |
244
|
|
|
} |
245
|
|
|
|
246
|
|
|
// If we were successful in adding the placefinder |
247
|
|
|
if (placefinder.parentNode) { |
248
|
|
|
var $_placefinder = $(placefinder); |
249
|
|
|
|
250
|
|
|
// offset() returns the top offset inside the total iframe, so we need the vertical scroll |
251
|
|
|
// value to adjust back to main window position |
252
|
|
|
// wizzy_height = $('#' + oMentions.opts.editor_id).parent().find('iframe').height(), |
253
|
|
|
// wizzy_window = $('#' + oMentions.opts.editor_id).parent().find('iframe').contents().height(), |
254
|
|
|
var wizzy_scroll = $('#' + oMentions.opts.editor_id).parent().find('iframe').contents().scrollTop(); |
255
|
|
|
|
256
|
|
|
// Determine its Location in the iframe |
257
|
|
|
offset = $_placefinder.offset(); |
258
|
|
|
|
259
|
|
|
// If we have scrolled, then we also need to account for those offsets |
260
|
|
|
offset.top -= wizzy_scroll; |
261
|
|
|
offset.top += $_placefinder.height(); |
262
|
|
|
|
263
|
|
|
// Remove our placefinder |
264
|
|
|
$_placefinder.remove(); |
265
|
|
|
} |
266
|
|
|
|
267
|
|
|
// Put things back just like we found them |
268
|
|
|
rangeHelper.restoreRange(); |
269
|
|
|
|
270
|
|
|
// Add in the iframe's offset to get the final location. |
271
|
|
|
if (offset) { |
272
|
|
|
var iframeOffset = editor.getContentAreaContainer().offset(); |
273
|
|
|
|
274
|
|
|
// Some fudge for the kids |
275
|
|
|
offset.top += iframeOffset.top + 5; |
276
|
|
|
offset.left += iframeOffset.left + 5; |
277
|
|
|
} |
278
|
|
|
|
279
|
|
|
return offset; |
280
|
|
|
} |
281
|
|
|
}; |
282
|
|
|
|
283
|
|
|
elk_Mentions.prototype.addUID = function(user_id, name) { |
284
|
|
|
this.opts._mentioned.append($('<input type="hidden" name="uid[]" />').val(user_id).attr('data-name', name)); |
285
|
|
|
}; |
286
|
|
|
|
287
|
|
|
/** |
288
|
|
|
* Private mention vars |
289
|
|
|
*/ |
290
|
|
|
elk_Mentions.prototype.defaults = { |
291
|
|
|
_names: [], |
292
|
|
|
_last_call: 0, |
293
|
|
|
_mentioned: $('<div id="mentioned" style="display: none;" />') |
294
|
|
|
}; |
295
|
|
|
|
296
|
|
|
/** |
297
|
|
|
* Holds all current mention (defaults + passed options) |
298
|
|
|
*/ |
299
|
|
|
elk_Mentions.prototype.opts = {}; |
300
|
|
|
|
301
|
|
|
/** |
302
|
|
|
* Mentioning plugin interface to SCEditor |
303
|
|
|
* - Called from the editor as a plugin |
304
|
|
|
* - Monitors events so we control the elk_mention |
305
|
|
|
*/ |
306
|
|
|
$.sceditor.plugins.mention = function() { |
307
|
|
|
var base = this, |
308
|
|
|
oMentions; |
309
|
|
|
|
310
|
|
|
base.init = function() { |
311
|
|
|
// Grab this instance for use use in oMentions |
312
|
|
|
editor = this; |
313
|
|
|
}; |
314
|
|
|
|
315
|
|
|
/** |
316
|
|
|
* Initialize, called when sceditor starts and initializes plugins |
317
|
|
|
*/ |
318
|
|
|
base.signalReady = function() { |
319
|
|
|
// Init the mention instance, load in the options |
320
|
|
|
oMentions = new elk_Mentions(this.opts.mentionOptions); |
321
|
|
|
|
322
|
|
|
var $option_eid = $('#' + oMentions.opts.editor_id); |
323
|
|
|
|
324
|
|
|
// Adds the selector to the list of known "mentioner" |
325
|
|
|
add_elk_mention(oMentions.opts.editor_id, {isPlugin: true}); |
326
|
|
|
oMentions.attachAtWho(oMentions, $option_eid.parent().find('textarea')); |
327
|
|
|
|
328
|
|
|
// Using wysiwyg, then lets attach atwho to it |
329
|
|
|
var instance = $option_eid.sceditor('instance'); |
330
|
|
|
if (!instance.opts.runWithoutWysiwygSupport) |
331
|
|
|
{ |
332
|
|
|
// We need to monitor the iframe window and body to text input |
333
|
|
|
var oIframe = $option_eid.parent().find('iframe')[0], |
334
|
|
|
oIframeWindow = oIframe.contentWindow, |
335
|
|
|
oIframeBody = $(oIframe.contentDocument.body); |
336
|
|
|
|
337
|
|
|
oMentions.attachAtWho(oMentions, oIframeBody, oIframeWindow); |
338
|
|
|
} |
339
|
|
|
}; |
340
|
|
|
}; |
341
|
|
|
})(jQuery, window, document); |
This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.